/*-------------------------------------------------------------------------
 *
 * deparse_text_search.c
 *	  All routines to deparse text search statements.
 *	  This file contains all entry points specific for text search statement deparsing.
 *
 * Copyright (c) Citus Data, Inc.
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "catalog/namespace.h"
#include "commands/defrem.h"
#include "utils/builtins.h"

#include "distributed/citus_ruleutils.h"
#include "distributed/deparser.h"
#include "distributed/listutils.h"

static void AppendDefElemList(StringInfo buf, List* defelems, char* objectName);

static void AppendStringInfoTokentypeList(StringInfo buf, List* tokentypes);
static void AppendStringInfoDictnames(StringInfo buf, List* dicts);

/*
 * DeparseCreateTextSearchConfigurationStmt returns the sql for a DefineStmt defining a
 * TEXT SEARCH CONFIGURATION
 *
 * Although the syntax is mutually exclusive on the two arguments that can be passed in
 * the deparser will syntactically correct multiple definitions if provided. *
 */
char* DeparseCreateTextSearchConfigurationStmt(Node* node)
{
    DefineStmt* stmt = castNode(DefineStmt, node);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    const char* identifier = NameListToQuotedString(stmt->defnames);
    appendStringInfo(&buf, "CREATE TEXT SEARCH CONFIGURATION %s ", identifier);
    appendStringInfoString(&buf, "(");
    AppendDefElemList(&buf, stmt->definition, "CONFIGURATION");
    appendStringInfoString(&buf, ");");

    return buf.data;
}

/*
 * DeparseCreateTextSearchDictionaryStmt returns the sql for a DefineStmt defining a
 * TEXT SEARCH DICTIONARY
 *
 * Although the syntax is mutually exclusive on the two arguments that can be passed in
 * the deparser will syntactically correct multiple definitions if provided. *
 */
char* DeparseCreateTextSearchDictionaryStmt(Node* node)
{
    DefineStmt* stmt = castNode(DefineStmt, node);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    const char* identifier = NameListToQuotedString(stmt->defnames);
    appendStringInfo(&buf, "CREATE TEXT SEARCH DICTIONARY %s ", identifier);
    appendStringInfoString(&buf, "(");
    AppendDefElemList(&buf, stmt->definition, "DICTIONARY");
    appendStringInfoString(&buf, ");");

    return buf.data;
}

/*
 * AppendDefElemList is a helper to append a comma separated list of definitions to a
 * define statement.
 *
 * The extra objectName parameter is used to create meaningful error messages.
 */
static void AppendDefElemList(StringInfo buf, List* defelems, char* objectName)
{
    DefElem* defelem = NULL;
    bool first = true;
    foreach_declared_ptr(defelem, defelems)
    {
        if (!first) {
            appendStringInfoString(buf, ", ");
        }
        first = false;

        /*
         * There are some operations that can omit the argument. In that case, we only use
         * the defname.
         *
         * For example, omitting [ = value ] in the next query results in resetting the
         * option to defaults:
         * ALTER TEXT SEARCH DICTIONARY name ( option [ = value ] );
         */
        if (defelem->arg == NULL) {
            appendStringInfo(buf, "%s", defelem->defname);
            continue;
        }

        /* extract value from defelem */
        const char* value = defGetString(defelem);

        /* stringify */
        appendStringInfo(buf, "%s = %s", defelem->defname, value);
    }
}

/*
 * DeparseDropTextSearchConfigurationStmt returns the sql representation for a DROP TEXT
 * SEARCH CONFIGURATION ... statment. Supports dropping multiple configurations at once.
 */
char* DeparseDropTextSearchConfigurationStmt(Node* node)
{
    DropStmt* stmt = castNode(DropStmt, node);
    Assert(stmt->removeType == OBJECT_TSCONFIGURATION);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfoString(&buf, "DROP TEXT SEARCH CONFIGURATION ");
    List* nameList = NIL;
    bool first = true;
    foreach_declared_ptr(nameList, stmt->objects)
    {
        if (!first) {
            appendStringInfoString(&buf, ", ");
        }
        first = false;

        appendStringInfoString(&buf, NameListToQuotedString(nameList));
    }

    if (stmt->behavior == DROP_CASCADE) {
        appendStringInfoString(&buf, " CASCADE");
    }

    appendStringInfoString(&buf, ";");

    return buf.data;
}

/*
 * DeparseDropTextSearchDictionaryStmt returns the sql representation for a DROP TEXT
 * SEARCH DICTIONARY ... statment. Supports dropping multiple dictionaries at once.
 */
char* DeparseDropTextSearchDictionaryStmt(Node* node)
{
    DropStmt* stmt = castNode(DropStmt, node);
    Assert(stmt->removeType == OBJECT_TSDICTIONARY);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfoString(&buf, "DROP TEXT SEARCH DICTIONARY ");
    List* nameList = NIL;
    bool first = true;
    foreach_declared_ptr(nameList, stmt->objects)
    {
        if (!first) {
            appendStringInfoString(&buf, ", ");
        }
        first = false;

        appendStringInfoString(&buf, NameListToQuotedString(nameList));
    }

    if (stmt->behavior == DROP_CASCADE) {
        appendStringInfoString(&buf, " CASCADE");
    }

    appendStringInfoString(&buf, ";");

    return buf.data;
}

/*
 * DeparseRenameTextSearchConfigurationStmt returns the sql representation of a ALTER TEXT
 * SEARCH CONFIGURATION ... RENAME TO ... statement.
 */
char* DeparseRenameTextSearchConfigurationStmt(Node* node)
{
    RenameStmt* stmt = castNode(RenameStmt, node);
    Assert(stmt->renameType == OBJECT_TSCONFIGURATION);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    char* identifier = NameListToQuotedString(castNode(List, stmt->object));
    appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s RENAME TO %s;", identifier,
                     quote_identifier(stmt->newname));

    return buf.data;
}

/*
 * DeparseRenameTextSearchDictionaryStmt returns the sql representation of a ALTER TEXT
 * SEARCH DICTIONARY ... RENAME TO ... statement.
 */
char* DeparseRenameTextSearchDictionaryStmt(Node* node)
{
    RenameStmt* stmt = castNode(RenameStmt, node);
    Assert(stmt->renameType == OBJECT_TSDICTIONARY);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    char* identifier = NameListToQuotedString(castNode(List, stmt->object));
    appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s RENAME TO %s;", identifier,
                     quote_identifier(stmt->newname));

    return buf.data;
}

/*
 * DeparseAlterTextSearchConfigurationStmt returns the sql representation of any generic
 * ALTER TEXT SEARCH CONFIGURATION .... statement. The statements supported include:
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ADD MAPPING FOR [, ...] WITH [, ...]
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ALTER MAPPING FOR [, ...] WITH [, ...]
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ALTER MAPPING REPLACE ... WITH ...
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ALTER MAPPING FOR [, ...] REPLACE ... WITH ...
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... DROP MAPPING [ IF EXISTS ] FOR ...
 */
char* DeparseAlterTextSearchConfigurationStmt(Node* node)
{
    AlterTSConfigurationStmt* stmt = castNode(AlterTSConfigurationStmt, node);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    char* identifier = NameListToQuotedString(castNode(List, stmt->cfgname));
    appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s", identifier);

    switch (stmt->kind) {
        case ALTER_TSCONFIG_ADD_MAPPING: {
            appendStringInfoString(&buf, " ADD MAPPING FOR ");
            AppendStringInfoTokentypeList(&buf, stmt->tokentype);

            appendStringInfoString(&buf, " WITH ");
            AppendStringInfoDictnames(&buf, stmt->dicts);

            break;
        }

        case ALTER_TSCONFIG_ALTER_MAPPING_FOR_TOKEN: {
            appendStringInfoString(&buf, " ALTER MAPPING FOR ");
            AppendStringInfoTokentypeList(&buf, stmt->tokentype);

            appendStringInfoString(&buf, " WITH ");
            AppendStringInfoDictnames(&buf, stmt->dicts);

            break;
        }

        case ALTER_TSCONFIG_REPLACE_DICT:
        case ALTER_TSCONFIG_REPLACE_DICT_FOR_TOKEN: {
            appendStringInfoString(&buf, " ALTER MAPPING");
            if (list_length(stmt->tokentype) > 0) {
                appendStringInfoString(&buf, " FOR ");
                AppendStringInfoTokentypeList(&buf, stmt->tokentype);
            }

            if (list_length(stmt->dicts) != 2) {
                elog(ERROR,
                     "unexpected number of dictionaries while deparsing ALTER "
                     "TEXT SEARCH CONFIGURATION ... ALTER MAPPING [FOR ...] REPLACE "
                     "statement.");
            }

            appendStringInfo(
                &buf, " REPLACE %s",
                NameListToQuotedString(static_cast<List*>(linitial(stmt->dicts))));

            appendStringInfo(
                &buf, " WITH %s",
                NameListToQuotedString(static_cast<List*>(lsecond(stmt->dicts))));

            break;
        }

        case ALTER_TSCONFIG_DROP_MAPPING: {
            appendStringInfoString(&buf, " DROP MAPPING");

            if (stmt->missing_ok) {
                appendStringInfoString(&buf, " IF EXISTS");
            }

            appendStringInfoString(&buf, " FOR ");
            AppendStringInfoTokentypeList(&buf, stmt->tokentype);
            break;
        }

        default: {
            elog(ERROR, "unable to deparse unsupported ALTER TEXT SEARCH STATEMENT");
        }
    }

    appendStringInfoString(&buf, ";");

    return buf.data;
}

/*
 * DeparseAlterTextSearchConfigurationStmt returns the sql representation of any generic
 * ALTER TEXT SEARCH DICTIONARY .... statement. The statements supported include
 * - ALTER TEXT SEARCH DICTIONARY name ( option [ = value ] [, ... ] )
 */
char* DeparseAlterTextSearchDictionaryStmt(Node* node)
{
    AlterTSDictionaryStmt* stmt = castNode(AlterTSDictionaryStmt, node);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    char* identifier = NameListToQuotedString(castNode(List, stmt->dictname));
    appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s ( ", identifier);

    AppendDefElemList(&buf, stmt->options, "DICTIONARY");
    appendStringInfoString(&buf, " );");
    return buf.data;
}

/*
 * DeparseAlterTextSearchConfigurationSchemaStmt returns the sql statement representing
 * ALTER TEXT SEARCH CONFIGURATION ... SET SCHEMA ... statements.
 */
char* DeparseAlterTextSearchConfigurationSchemaStmt(Node* node)
{
    AlterObjectSchemaStmt* stmt = castNode(AlterObjectSchemaStmt, node);
    Assert(stmt->objectType == OBJECT_TSCONFIGURATION);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s SET SCHEMA %s;",
                     NameListToQuotedString(castNode(List, stmt->object)),
                     quote_identifier(stmt->newschema));

    return buf.data;
}

/*
 * DeparseAlterTextSearchDictionarySchemaStmt returns the sql statement representing ALTER
 * TEXT SEARCH DICTIONARY ... SET SCHEMA ... statements.
 */
char* DeparseAlterTextSearchDictionarySchemaStmt(Node* node)
{
    AlterObjectSchemaStmt* stmt = castNode(AlterObjectSchemaStmt, node);
    Assert(stmt->objectType == OBJECT_TSDICTIONARY);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s SET SCHEMA %s;",
                     NameListToQuotedString(castNode(List, stmt->object)),
                     quote_identifier(stmt->newschema));

    return buf.data;
}

/*
 * DeparseTextSearchConfigurationCommentStmt returns the sql statement representing
 * COMMENT ON TEXT SEARCH CONFIGURATION ... IS ...
 */
char* DeparseTextSearchConfigurationCommentStmt(Node* node)
{
    CommentStmt* stmt = castNode(CommentStmt, node);
    Assert(stmt->objtype == OBJECT_TSCONFIGURATION);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfo(&buf, "COMMENT ON TEXT SEARCH CONFIGURATION %s IS ",
                     NameListToQuotedString(castNode(List, stmt->objname)));

    if (stmt->comment == NULL) {
        appendStringInfoString(&buf, "NULL");
    } else {
        appendStringInfoString(&buf, quote_literal_cstr(stmt->comment));
    }

    appendStringInfoString(&buf, ";");

    return buf.data;
}

/*
 * DeparseTextSearchDictionaryCommentStmt returns the sql statement representing
 * COMMENT ON TEXT SEARCH DICTIONARY ... IS ...
 */
char* DeparseTextSearchDictionaryCommentStmt(Node* node)
{
    CommentStmt* stmt = castNode(CommentStmt, node);
    Assert(stmt->objtype == OBJECT_TSDICTIONARY);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfo(&buf, "COMMENT ON TEXT SEARCH DICTIONARY %s IS ",
                     NameListToQuotedString(castNode(List, stmt->objname)));

    if (stmt->comment == NULL) {
        appendStringInfoString(&buf, "NULL");
    } else {
        appendStringInfoString(&buf, quote_literal_cstr(stmt->comment));
    }

    appendStringInfoString(&buf, ";");

    return buf.data;
}

/*
 * AppendStringInfoTokentypeList specializes in adding a comma separated list of
 * token_tyoe's to TEXT SEARCH CONFIGURATION commands
 */
static void AppendStringInfoTokentypeList(StringInfo buf, List* tokentypes)
{
    List* tokentype = NULL;
    bool first = true;
    foreach_declared_ptr(tokentype, tokentypes)
    {
        if (nodeTag(tokentype) != T_String) {
            elog(ERROR,
                 "unexpected tokentype for deparsing in text search configuration");
        }

        if (!first) {
            appendStringInfoString(buf, ", ");
        }
        first = false;

        appendStringInfoString(buf, strVal(tokentype));
    }
}

/*
 * AppendStringInfoDictnames specializes in appending a comma separated list of
 * dictionaries to TEXT SEARCH CONFIGURATION commands.
 */
static void AppendStringInfoDictnames(StringInfo buf, List* dicts)
{
    List* dictNames = NIL;
    bool first = true;
    foreach_declared_ptr(dictNames, dicts)
    {
        if (!first) {
            appendStringInfoString(buf, ", ");
        }
        first = false;

        char* dictIdentifier = NameListToQuotedString(dictNames);
        appendStringInfoString(buf, dictIdentifier);
    }
}

/*
 * DeparseAlterTextSearchConfigurationOwnerStmt returns the sql statement representing
 * ALTER TEXT SEARCH CONFIGURATION ... ONWER TO ... commands.
 */
char* DeparseAlterTextSearchConfigurationOwnerStmt(Node* node)
{
    AlterOwnerStmt* stmt = castNode(AlterOwnerStmt, node);
    Assert(stmt->objectType == OBJECT_TSCONFIGURATION);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s OWNER TO %s;",
                     NameListToQuotedString(castNode(List, stmt->object)),
                     RoleSpecString(stmt->newowner, true));

    return buf.data;
}

/*
 * DeparseAlterTextSearchDictionaryOwnerStmt returns the sql statement representing ALTER
 * TEXT SEARCH DICTIONARY ... ONWER TO ... commands.
 */
char* DeparseAlterTextSearchDictionaryOwnerStmt(Node* node)
{
    AlterOwnerStmt* stmt = castNode(AlterOwnerStmt, node);
    Assert(stmt->objectType == OBJECT_TSDICTIONARY);

    StringInfoData buf = {0};
    initStringInfo(&buf);

    appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s OWNER TO %s;",
                     NameListToQuotedString(castNode(List, stmt->object)),
                     RoleSpecString(stmt->newowner, true));

    return buf.data;
}
